SwiftUIでHome Screen Quick Actions に対応してみた
iOS 13以降のデバイスのホーム画面では、アプリのアイコンを長押しすると、ホーム画面のクイックアクションを表示できます(3D Touch デバイスでは、ユーザーはアイコンを短く押す)。
引用: Apple公式: iPhoneでクイックアクションを実行する
このようにホーム画面から簡単なアプリの操作が出来る機能で、楽しそうなので今回はSwiftUIで試してみることにしました。
環境
- Xcode 13.3
はじめに
Info.plistにクイックアクションを定義することで静的なクイックアクションを定義することが出来ますが、今回はInfo.plistを使用しない方法でクイックアクションを実装しました。
作ったもの
クイックアクションで選択したアクションによって、開かれたアプリのUIを変更するというシンプルなものになります。
QuickAction
まずはクイックアクションを設定するために列挙型のQuickAction
を作成します。
import UIKit enum QuickAction: String, CaseIterable { case flame = "Flame" case water = "Water" case thunder = "Thunder" init?(shortcutItem: UIApplicationShortcutItem?) { guard let shortcutItem = shortcutItem, let action = QuickAction(rawValue: shortcutItem.type) else { return nil } self = action } var imageName: String { switch self { case .flame: return "flame" case .water: return "drop" case .thunder: return "bolt" } } var shortcutItem: UIApplicationShortcutItem { switch self { case .flame: return UIMutableApplicationShortcutItem( type: self.rawValue, localizedTitle: self.rawValue, localizedSubtitle: "Viewが熱く燃え上がります", icon: UIApplicationShortcutIcon(systemImageName: self.imageName) ) case .water: return UIMutableApplicationShortcutItem( type: self.rawValue, localizedTitle: self.rawValue, localizedSubtitle: "Viewが水に満たされます", icon: UIApplicationShortcutIcon(systemImageName: self.imageName) ) case .thunder: return UIMutableApplicationShortcutItem( type: self.rawValue, localizedTitle: self.rawValue, localizedSubtitle: "Viewが雷光により輝きます", icon: UIApplicationShortcutIcon(systemImageName: self.imageName) ) } } }
init
後で記述しますが、クイックアクションからアプリを開いた際にUIApplicationShortcutItem
を取得出来ます。そのUIApplicationShortcutItem.type
を使用して選択されたアクションを初期化しています。
init?(shortcutItem: UIApplicationShortcutItem?) { guard let shortcutItem = shortcutItem, let action = QuickAction(rawValue: shortcutItem.type) else { return nil } self = action }
imageName
UIApplicationShortcutItem
の画像として使用するSystem Imageの名前になります。
var imageName: String { switch self { case .flame: return "flame" case .water: return "drop" case .thunder: return "bolt" } }
shortcutItem
それぞれのケースに該当するUIMutableApplicationShortcutItem
を返しています。UIMutableApplicationShortcutItem
を初期化することで変更可能な動的クイックアクションを作成することが出来ます。
var shortcutItem: UIApplicationShortcutItem { switch self { case .flame: return UIMutableApplicationShortcutItem( type: self.rawValue, localizedTitle: self.rawValue, localizedSubtitle: "Viewが熱く燃え上がります", icon: UIApplicationShortcutIcon(systemImageName: self.imageName) )
UIMutableApplicationSHortcutItem
- type
- 実行するクイックアクションの種類を識別するために使用する文字列
- localizedTitle
- ユーザーに表示されるタイトル
- localizedSubTitle
- ユーザーに表示されるサブタイトル
- icon
- クイックアクションのアイコン
- userInfo
- クイックアクション実行時に提供できるユーザー情報
実際のクイックアクションと照らし合わせるとこのようになります。
QuickActionState
QuickAction
の状態を管理するQuickActionState
を作成します。
import UIKit class QuickActionState: ObservableObject { static let shared = QuickActionState() private init() {} @Published var selectedAction: QuickAction? private var isEnteredFromQuickAction = false func setActions() { let shortcutItems = QuickAction.allCases.map { $0.shortcutItem } UIApplication.shared.shortcutItems = shortcutItems } func selectAction(by shortcutItem: UIApplicationShortcutItem?) { let action = QuickAction(shortcutItem: shortcutItem) selectedAction = action isEnteredFromQuickAction = action == nil ? false : true } func removeSelectedActionIfNeeded() { if isEnteredFromQuickAction { isEnteredFromQuickAction = false return } selectedAction = nil } }
プロパティ
selectedAction
選択されたクイックアクションをパブリッシュする為の変数です。
@Published var selectedAction: QuickAction?
isEnteredFromQuickAction
今回はクイックアクションからアプリを開いたかどうかでUIを変更する為にそのフラグを用意しました。
private var isEnteredFromQuickAction = false
ファンクション
setActions
定義したクイックアクションをUIApplication.shared.shortcutItems
に渡す関数です。
func setActions() { let shortcutItems = QuickAction.allCases.map { $0.shortcutItem } UIApplication.shared.shortcutItems = shortcutItems }
selectAction
クイックアクションが選択された場合にそのクイックアクションに紐づくUIApplicationShortcutItem
が渡ってきます。そのUIApplicationShortcutItem
からQuickAction
を生成して、selectedAction
に渡しています。またQuickAction
がnil
では無いということは、クイックアクションからアプリを開いたということなので、isEnteredFromQuickAction
のフラグを切り替えています。
func selectAction(by shortcutItem: UIApplicationShortcutItem?) { let action = QuickAction(shortcutItem: shortcutItem) selectedAction = action isEnteredFromQuickAction = action == nil ? false : true }
removeSelectedActionIfNeeded
クイックアクションからのアプリの立ち上げでは無い場合は、選択されたクイックアクションは無い為、selectedAction
をnil
にしています。
func removeSelectedActionIfNeeded() { if isEnteredFromQuickAction { isEnteredFromQuickAction = false return } selectedAction = nil }
ContentView
実際に選択されたクイックアクションによって表示を変更するViewを作成します。
import SwiftUI struct ContentView: View { @EnvironmentObject var quickActionState: QuickActionState private var backgroundColor: Color { switch quickActionState.selectedAction { case .flame: return .red case .water: return .blue case .thunder: return .yellow case .none: return .white } } var body: some View { ZStack { Rectangle() .fill(backgroundColor) .ignoresSafeArea() if let action = quickActionState.selectedAction { Image(systemName: action.imageName) .resizable() .frame(width: 150, height: 200) .foregroundColor(action == .thunder ? .black : .white) } } } }
プロパティ
quickActionState
選択されたクイックアクションの状態によってViewを変更する為、EnvironmentObject
を定義します。
@EnvironmentObject var quickActionState: QuickActionState
backgroundColor
選択されているクイックアクションによって動的に変更する背景色を用意しておきます。
private var backgroundColor: Color { switch quickActionState.selectedAction { case .flame: return .red case .water: return .blue case .thunder: return .yellow case .none: return .white } }
body
背景色には上記で用意したbackgroundColor
を使用します。表示するImage
のシンボルにはQuickAction.imageName
を渡しています。selectedAction
がnil
の場合にはImage
は表示されません。
var body: some View { ZStack { Rectangle() .fill(backgroundColor) .ignoresSafeArea() if let action = quickActionState.selectedAction { Image(systemName: action.imageName) .resizable() .frame(width: 150, height: 200) .foregroundColor(action == .thunder ? .black : .white) } } }
App
エントリーポイントとなるApp
部分の実装です。
import SwiftUI @main struct QuickActionsSampleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Environment(\.scenePhase) var scenePhase private let quickActionState = QuickActionState.shared var body: some Scene { WindowGroup { ContentView() .environmentObject(quickActionState) } .onChange(of: scenePhase) { newValue in switch newValue { case .active: quickActionState.removeSelectedActionIfNeeded() case .background: quickActionState.setActions() case .inactive: break @unknown default: fatalError() } } } }
プロパティ
appDelegate
UIApplicationDelegateAdaptor
でAppDelegate
を呼べるようにしています。
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
scenePhase
scenePhase
がアクティブなのかバックグラウンドなのかで処理を行いたいのでscenePhase
変数を用意します。
@Environment(\.scenePhase) var scenePhase
quickActionState
クイックアクションの状態を見るQuickActionState
を宣言します。
private let quickActionState = QuickActionState.shared
body
ContentView
はEnvironmentObject
のquickActionState
を渡す必要がある為、渡しています。
また、scenePhase
の状態の変化を見て、active
ならremoveSelectedActionIfNeeded()
を実行し、background
になる際はsetActions()
を実行しています。
var body: some Scene { WindowGroup { ContentView() .environmentObject(quickActionState) } .onChange(of: scenePhase) { newValue in switch newValue { case .active: quickActionState.removeSelectedActionIfNeeded() case .background: quickActionState.setActions() case .inactive: break @unknown default: fatalError() } } }
今回は.background
になる毎にquickAction
をセットする必要はないですが、クイックアクションの文言やアイコン、タイプ等を選択されているページによって変えたい時にはこの.background
になる毎にクイックアクションをセットすると良さそうです。
AppDelegate
import UIKit class AppDelegate: NSObject, UIApplicationDelegate { private let quickActionState = QuickActionState.shared func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { quickActionState.selectAction(by: options.shortcutItem) let configuration = UISceneConfiguration( name: connectingSceneSession.configuration.name, sessionRole: connectingSceneSession.role ) configuration.delegateClass = SceneDelegate.self return configuration } }
configurationForConnecting
この関数は、Sceneが作成される時に、UIKitのための設定を行います。
このメソッドは起動時に一度呼ばれます。
この第三引数options
は選択されたクイックアクションに紐づくUIApplicationShortcutItem
を持っている為、そのshortcutItem
をselectAction
に渡して実行しています。
またSceneDelegate
を呼ぶために、configuration.delegateClass
に下記で説明するSceneDelegate
を渡しています。
SceneDelegate
import UIKit class SceneDelegate: NSObject, UIWindowSceneDelegate { private let quickActionState = QuickActionState.shared func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { quickActionState.selectAction(by: options.shortcutItem) completionHandler(true) } }
この関数では、クイックアクションが実行された際に、ユーザーが選んだクイックアクションに紐づくUIApplicationShortcutItem
が渡ってきます。そのshortcutItem
をselectAction
に渡して実行しています。
これでクイックアクションによってViewを切り替えれるようになりました。
動的クイックアクションの考慮事項
今回はInfo.plistを使用しない動的クリックアクションを設定する方法を使用しましたが、UIApplicationShortcutItemのApp Launch and App Update Considerations for Quick Actionsの項目を見ると、下記の考慮事項が記載ありました。
ユーザーが最初にアプリをインストールした後、最初に起動する前に、ホーム画面のアイコンを押すと、アプリの静的なクイックアクションのみが表示されます。最初の起動後、動的クイックアクション (いずれかを定義しており、リストに対応する余地がある場合) も表示されます。
動的クイックアクションを使用する場合、そのインストール済みのアプリを一度も起動していない時は、クイックアクションが表示されないようです。クイックアクションをセットする処理を書いておけば、一度起動すると設定されクイックアクションが表示されるようです。
おわりに
今回は単純にクイックアクションからViewの見た目を変えるだけでしたが、メモアプリではクイックアクションから直接新規追加のページに飛ばしたり、最後に見たメモを保持しておき、最後に見たメモに飛ばすことも出来そうですね。あまり使用したことはなかったですが、案外便利な機能のような気がしました! 少しずつ使っていきたいと思います。
何かこの機能を使って面白いことが出来ないか考え中です、、。
参考
- Add Home Screen Quick Actions
- 3D Touch の Home Screen Quick Actions に対応する
- Home Screen Quick Actions(3D Touch)でアプリを開く
- 【SwiftUI】静的 Home Screen Quick Actions(アプリアイコン長押しメニュー)
- UIApplicationShortcutItem
- UIMutableApplicationShortcutItem
- application(_ :configurationForConnecting :options: )メソッドについて調べたことのまとめ
- windowScene(_:performActionFor:completionHandler:)